{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/home/joaquin/Documents/GitHub/skforecast\n"
     ]
    }
   ],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2\n",
    "import sys\n",
    "from pathlib import Path\n",
    "path = str(Path.cwd().parent)\n",
    "print(path)\n",
    "sys.path.insert(1, path)\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "import platform\n",
    "import psutil\n",
    "import threadpoolctl\n",
    "import sklearn\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import lightgbm\n",
    "import skforecast\n",
    "from packaging.version import parse as parse_version\n",
    "\n",
    "from lightgbm import LGBMRegressor\n",
    "from sklearn.linear_model import LinearRegression\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from skforecast.utils import check_predict_input\n",
    "from skforecast.recursive import ForecasterRecursive\n",
    "from skforecast.model_selection import TimeSeriesFold, backtesting_forecaster, backtesting_forecaster_multiseries\n",
    "\n",
    "%load_ext pyinstrument\n",
    "%load_ext line_profiler"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install pyinstrument\n",
    "# !pip install line_profiler\n",
    "# !pip install lightgbm==4.6.0\n",
    "# !pip install -U scikit-learn==1.6.1 lightgbm==4.6.0 pandas==2.2.3 numpy==2.1.3\n",
    "\n",
    "# conda create -n skforecast_benchmark_3_12_9 python=3.12.9"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Python version: 3.12.9\n",
      "skforecast version: 0.17.0\n",
      "scikit-learn version: 1.7.1\n",
      "lightgbm version: 4.6.0\n",
      "pandas version: 2.3.1\n",
      "numpy version: 2.2.6\n",
      "Computer network name: joaquin-HP-ProBook-440-G6\n",
      "Processor type: x86_64\n",
      "Platform type: Linux-6.14.0-27-generic-x86_64-with-glibc2.39\n",
      "Operating system: Linux\n",
      "Operating system release: 6.14.0-27-generic\n",
      "Operating system version: #27~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 17:38:49 UTC 2\n",
      "Number of physical cores: 4\n",
      "Number of logical cores: 8\n",
      "Threadpools: [{'user_api': 'blas', 'internal_api': 'openblas', 'num_threads': 8, 'prefix': 'libscipy_openblas', 'filepath': '/home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/numpy.libs/libscipy_openblas64_-56d6093b.so', 'version': '0.3.29', 'threading_layer': 'pthreads', 'architecture': 'Haswell'}, {'user_api': 'blas', 'internal_api': 'openblas', 'num_threads': 8, 'prefix': 'libscipy_openblas', 'filepath': '/home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/scipy.libs/libscipy_openblas-68440149.so', 'version': '0.3.28', 'threading_layer': 'pthreads', 'architecture': 'Haswell'}, {'user_api': 'openmp', 'internal_api': 'openmp', 'num_threads': 8, 'prefix': 'libgomp', 'filepath': '/home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/scikit_learn.libs/libgomp-a34b3233.so.1.0.0', 'version': None}, {'user_api': 'openmp', 'internal_api': 'openmp', 'num_threads': 8, 'prefix': 'libgomp', 'filepath': '/home/joaquin/miniconda3/envs/skforecast_16_py12/lib/libgomp.so.1.0.0', 'version': None}]\n",
      "No BLAS info: module 'numpy.__config__' has no attribute 'get_info'\n"
     ]
    }
   ],
   "source": [
    "print(f\"Python version: {platform.python_version()}\")\n",
    "print(f\"skforecast version: {skforecast.__version__}\")\n",
    "print(f\"scikit-learn version: {sklearn.__version__}\")\n",
    "print(f\"lightgbm version: {lightgbm.__version__}\")\n",
    "print(f\"pandas version: {pd.__version__}\")\n",
    "print(f\"numpy version: {np.__version__}\")\n",
    "print(f\"Computer network name: {platform.node()}\")\n",
    "print(f\"Processor type: {platform.processor()}\")\n",
    "print(f\"Platform type: {platform.platform()}\")\n",
    "print(f\"Operating system: {platform.system()}\")\n",
    "print(f\"Operating system release: {platform.release()}\")\n",
    "print(f\"Operating system version: {platform.version()}\")\n",
    "print(f\"Number of physical cores: {psutil.cpu_count(logical=False)}\")\n",
    "print(f\"Number of logical cores: {psutil.cpu_count(logical=True)}\")\n",
    "print(\"Threadpools:\", threadpoolctl.threadpool_info())\n",
    "# Info BLAS detallada (NumPy >=1.26)\n",
    "try:\n",
    "    import numpy.__config__ as c\n",
    "    print(c.get_info('blas_opt_info'))\n",
    "except Exception as e:\n",
    "    print(\"No BLAS info:\", e)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Mock data for benchmarking\n",
    "# ==========================================================\n",
    "len_series = 2000\n",
    "rng = np.random.default_rng(321)\n",
    "y = pd.Series(\n",
    "        data = rng.normal(loc=20, scale=5, size=len_series),\n",
    "        index=pd.date_range(\n",
    "            start='2010-01-01',\n",
    "            periods=len_series,\n",
    "            freq='h'\n",
    "        ),\n",
    "        name='y'\n",
    "    )\n",
    "rng = np.random.default_rng(321)\n",
    "exog = pd.DataFrame(index=y.index)\n",
    "exog['day_of_week'] = exog.index.dayofweek\n",
    "exog['week_of_year'] = exog.index.isocalendar().week.astype(int)\n",
    "exog['month'] = exog.index.month\n",
    "exog_prediction = pd.DataFrame(\n",
    "                    index=pd.date_range(\n",
    "                        start=exog.index.max() + pd.Timedelta(hours=1),\n",
    "                        periods=100,\n",
    "                        freq='h'\n",
    "                    )\n",
    "                 ) \n",
    "exog_prediction['day_of_week'] = exog_prediction.index.dayofweek\n",
    "exog_prediction['week_of_year'] = exog_prediction.index.isocalendar().week.astype(int)\n",
    "exog_prediction['month'] = exog_prediction.index.month"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hash of y: f1ba70f786d503ec78f1cd228723bf3d073a5dfe3345f2faa26800d02cabe439\n",
      "Hash of exog: 10fb78eb4190dd80df8de7bff30fa2fc8e974c5da5064ef2eaf6b69a5c17cf3d\n",
      "Hash of exog_prediction: b533b86d8d547d466cdb52ceba8ae4ce5261765906d976d76078c6f2cfa0c5e4\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "dtype('float64')"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "day_of_week     int32\n",
       "week_of_year    int64\n",
       "month           int32\n",
       "dtype: object"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "day_of_week     int32\n",
       "week_of_year    int64\n",
       "month           int32\n",
       "dtype: object"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import hashlib\n",
    "\n",
    "def hash_dataframe(df):\n",
    "    row_hashes = pd.util.hash_pandas_object(df, index=True).values\n",
    "    combined_hash = hashlib.sha256(row_hashes.tobytes()).hexdigest()\n",
    "    return combined_hash\n",
    "\n",
    "hash_y = hash_dataframe(y)\n",
    "hash_exog = hash_dataframe(exog)\n",
    "hash_exog_prediction = hash_dataframe(exog_prediction)\n",
    "print(f\"Hash of y: {hash_y}\")\n",
    "print(f\"Hash of exog: {hash_exog}\")\n",
    "print(f\"Hash of exog_prediction: {hash_exog_prediction}\")\n",
    "display(y.dtypes)\n",
    "display(exog.dtypes)\n",
    "display(exog_prediction.dtypes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "regressor = LGBMRegressor(random_state=8520, verbose=-1)\n",
    "regressor = LinearRegression()\n",
    "forecaster = ForecasterRecursive(\n",
    "    regressor=regressor,\n",
    "    lags=50,\n",
    "    transformer_y=None,\n",
    "    transformer_exog=None,\n",
    ")\n",
    "forecaster.fit(y=y, exog=exog)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## skforecast 0.17.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "439 μs ± 72.4 μs per loop (mean ± std. dev. of 100 runs, 200 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit -n 200 -r 100\n",
    "\n",
    "check_predict_input(\n",
    "    forecaster_name  = type(forecaster).__name__,\n",
    "    steps            = 100,\n",
    "    is_fitted        = forecaster.is_fitted,\n",
    "    exog_in_         = forecaster.exog_in_,\n",
    "    index_type_      = forecaster.index_type_,\n",
    "    index_freq_      = forecaster.index_freq_,\n",
    "    window_size      = forecaster.window_size,\n",
    "    last_window      = forecaster.last_window_,\n",
    "    exog             = exog_prediction,\n",
    "    exog_names_in_   = forecaster.exog_names_in_,\n",
    "    interval         = None\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Timer unit: 1e-09 s\n",
      "\n",
      "Total time: 0.00215959 s\n",
      "File: /home/joaquin/Documents/GitHub/skforecast/skforecast/utils/utils.py\n",
      "Function: check_predict_input at line 776\n",
      "\n",
      "Line #      Hits         Time  Per Hit   % Time  Line Contents\n",
      "==============================================================\n",
      "   776                                           def check_predict_input(\n",
      "   777                                               forecaster_name: str,\n",
      "   778                                               steps: int | list[int],\n",
      "   779                                               is_fitted: bool,\n",
      "   780                                               exog_in_: bool,\n",
      "   781                                               index_type_: type,\n",
      "   782                                               index_freq_: str,\n",
      "   783                                               window_size: int,\n",
      "   784                                               last_window: pd.Series | pd.DataFrame | None,\n",
      "   785                                               last_window_exog: pd.Series | pd.DataFrame | None = None,\n",
      "   786                                               exog: pd.Series | pd.DataFrame | dict[str, pd.Series | pd.DataFrame] | None = None,\n",
      "   787                                               exog_names_in_: list[str] | None = None,\n",
      "   788                                               interval: list[float] | None = None,\n",
      "   789                                               alpha: float | None = None,\n",
      "   790                                               max_step: int | None = None,\n",
      "   791                                               levels: str | list[str] | None = None,\n",
      "   792                                               levels_forecaster: str | list[str] | None = None,\n",
      "   793                                               series_names_in_: list[str] | None = None,\n",
      "   794                                               encoding: str | None = None\n",
      "   795                                           ) -> None:\n",
      "   796                                               \"\"\"\n",
      "   797                                               Check all inputs of predict method. This is a helper function to validate\n",
      "   798                                               that inputs used in predict method match attributes of a forecaster already\n",
      "   799                                               trained.\n",
      "   800                                           \n",
      "   801                                               Parameters\n",
      "   802                                               ----------\n",
      "   803                                               forecaster_name : str\n",
      "   804                                                   Forecaster name.\n",
      "   805                                               steps : int, list\n",
      "   806                                                   Number of future steps predicted.\n",
      "   807                                               is_fitted: bool\n",
      "   808                                                   Tag to identify if the regressor has been fitted (trained).\n",
      "   809                                               exog_in_ : bool\n",
      "   810                                                   If the forecaster has been trained using exogenous variable/s.\n",
      "   811                                               index_type_ : type\n",
      "   812                                                   Type of index of the input used in training.\n",
      "   813                                               index_freq_ : str\n",
      "   814                                                   Frequency of Index of the input used in training.\n",
      "   815                                               window_size: int\n",
      "   816                                                   Size of the window needed to create the predictors. It is equal to \n",
      "   817                                                   `max_lag`.\n",
      "   818                                               last_window : pandas Series, pandas DataFrame, None\n",
      "   819                                                   Values of the series used to create the predictors (lags) need in the \n",
      "   820                                                   first iteration of prediction (t + 1).\n",
      "   821                                               last_window_exog : pandas Series, pandas DataFrame, default None\n",
      "   822                                                   Values of the exogenous variables aligned with `last_window` in \n",
      "   823                                                   ForecasterSarimax predictions.\n",
      "   824                                               exog : pandas Series, pandas DataFrame, dict, default None\n",
      "   825                                                   Exogenous variable/s included as predictor/s.\n",
      "   826                                               exog_names_in_ : list, default None\n",
      "   827                                                   Names of the exogenous variables used during training.\n",
      "   828                                               interval : list, tuple, default None\n",
      "   829                                                   Confidence of the prediction interval estimated. Sequence of percentiles\n",
      "   830                                                   to compute, which must be between 0 and 100 inclusive. For example, \n",
      "   831                                                   interval of 95% should be as `interval = [2.5, 97.5]`.\n",
      "   832                                               alpha : float, default None\n",
      "   833                                                   The confidence intervals used in ForecasterSarimax are (1 - alpha) %.\n",
      "   834                                               max_step: int, default None\n",
      "   835                                                   Maximum number of steps allowed (`ForecasterDirect` and \n",
      "   836                                                   `ForecasterDirectMultiVariate`).\n",
      "   837                                               levels : str, list, default None\n",
      "   838                                                   Time series to be predicted (`ForecasterRecursiveMultiSeries`\n",
      "   839                                                   and `ForecasterRnn).\n",
      "   840                                               levels_forecaster : str, list, default None\n",
      "   841                                                   Time series used as output data of a multiseries problem in a RNN problem\n",
      "   842                                                   (`ForecasterRnn`).\n",
      "   843                                               series_names_in_ : list, default None\n",
      "   844                                                   Names of the columns used during fit (`ForecasterRecursiveMultiSeries`, \n",
      "   845                                                   `ForecasterDirectMultiVariate` and `ForecasterRnn`).\n",
      "   846                                               encoding : str, default None\n",
      "   847                                                   Encoding used to identify the different series (`ForecasterRecursiveMultiSeries`).\n",
      "   848                                           \n",
      "   849                                               Returns\n",
      "   850                                               -------\n",
      "   851                                               None\n",
      "   852                                           \n",
      "   853                                               \"\"\"\n",
      "   854                                           \n",
      "   855         1       1973.0   1973.0      0.1      if not is_fitted:\n",
      "   856                                                   raise NotFittedError(\n",
      "   857                                                       \"This Forecaster instance is not fitted yet. Call `fit` with \"\n",
      "   858                                                       \"appropriate arguments before using predict.\"\n",
      "   859                                                   )\n",
      "   860                                           \n",
      "   861         1       3512.0   3512.0      0.2      if isinstance(steps, (int, np.integer)) and steps < 1:\n",
      "   862                                                   raise ValueError(\n",
      "   863                                                       f\"`steps` must be an integer greater than or equal to 1. Got {steps}.\"\n",
      "   864                                                   )\n",
      "   865                                           \n",
      "   866         1       1480.0   1480.0      0.1      if isinstance(steps, list) and min(steps) < 1:\n",
      "   867                                                   raise ValueError(\n",
      "   868                                                      f\"The minimum value of `steps` must be equal to or greater than 1. \"\n",
      "   869                                                      f\"Got {min(steps)}.\"\n",
      "   870                                                   )\n",
      "   871                                           \n",
      "   872         1        680.0    680.0      0.0      if max_step is not None:\n",
      "   873                                                   if max(steps) > max_step:\n",
      "   874                                                       raise ValueError(\n",
      "   875                                                           f\"The maximum value of `steps` must be less than or equal to \"\n",
      "   876                                                           f\"the value of steps defined when initializing the forecaster. \"\n",
      "   877                                                           f\"Got {max(steps)}, but the maximum is {max_step}.\"\n",
      "   878                                                       )\n",
      "   879                                           \n",
      "   880         1       1013.0   1013.0      0.0      if interval is not None or alpha is not None:\n",
      "   881                                                   check_interval(interval=interval, alpha=alpha)\n",
      "   882                                           \n",
      "   883         1       1412.0   1412.0      0.1      if forecaster_name in ['ForecasterRecursiveMultiSeries', 'ForecasterRnn']:\n",
      "   884                                                   if not isinstance(levels, (type(None), str, list)):\n",
      "   885                                                       raise TypeError(\n",
      "   886                                                           \"`levels` must be a `list` of column names, a `str` of a \"\n",
      "   887                                                           \"column name or `None`.\"\n",
      "   888                                                       )\n",
      "   889                                           \n",
      "   890                                                   levels_to_check = (\n",
      "   891                                                       levels_forecaster if forecaster_name == 'ForecasterRnn'\n",
      "   892                                                       else series_names_in_\n",
      "   893                                                   )\n",
      "   894                                                   unknown_levels = set(levels) - set(levels_to_check)\n",
      "   895                                                   if forecaster_name == 'ForecasterRnn':\n",
      "   896                                                       if len(unknown_levels) != 0:\n",
      "   897                                                           raise ValueError(\n",
      "   898                                                               f\"`levels` names must be included in the series used during fit \"\n",
      "   899                                                               f\"({levels_to_check}). Got {levels}.\"\n",
      "   900                                                           )\n",
      "   901                                                   else:\n",
      "   902                                                       if len(unknown_levels) != 0 and last_window is not None and encoding is not None:\n",
      "   903                                                           if encoding == 'onehot':\n",
      "   904                                                               warnings.warn(\n",
      "   905                                                                   f\"`levels` {unknown_levels} were not included in training. The resulting \"\n",
      "   906                                                                   f\"one-hot encoded columns for this feature will be all zeros.\",\n",
      "   907                                                                   UnknownLevelWarning\n",
      "   908                                                               )\n",
      "   909                                                           else:\n",
      "   910                                                               warnings.warn(\n",
      "   911                                                                   f\"`levels` {unknown_levels} were not included in training. \"\n",
      "   912                                                                   f\"Unknown levels are encoded as NaN, which may cause the \"\n",
      "   913                                                                   f\"prediction to fail if the regressor does not accept NaN values.\",\n",
      "   914                                                                   UnknownLevelWarning\n",
      "   915                                                               )\n",
      "   916                                           \n",
      "   917         1        916.0    916.0      0.0      if exog is None and exog_in_:\n",
      "   918                                                   raise ValueError(\n",
      "   919                                                       \"Forecaster trained with exogenous variable/s. \"\n",
      "   920                                                       \"Same variable/s must be provided when predicting.\"\n",
      "   921                                                   )\n",
      "   922                                           \n",
      "   923         1       5486.0   5486.0      0.3      if exog is not None and not exog_in_:\n",
      "   924                                                   raise ValueError(\n",
      "   925                                                       \"Forecaster trained without exogenous variable/s. \"\n",
      "   926                                                       \"`exog` must be `None` when predicting.\"\n",
      "   927                                                   )\n",
      "   928                                           \n",
      "   929                                               # Checks last_window\n",
      "   930                                               # Check last_window type (pd.Series or pd.DataFrame according to forecaster)\n",
      "   931         1       8706.0   8706.0      0.4      if isinstance(last_window, type(None)) and forecaster_name not in [\n",
      "   932                                                   'ForecasterRecursiveMultiSeries', \n",
      "   933                                                   'ForecasterRnn'\n",
      "   934                                               ]:\n",
      "   935                                                   raise ValueError(\n",
      "   936                                                       \"`last_window` was not stored during training. If you don't want \"\n",
      "   937                                                       \"to retrain the Forecaster, provide `last_window` as argument.\"\n",
      "   938                                                   )\n",
      "   939                                           \n",
      "   940         1       6528.0   6528.0      0.3      if forecaster_name in ['ForecasterRecursiveMultiSeries', \n",
      "   941                                                                      'ForecasterDirectMultiVariate',\n",
      "   942                                                                      'ForecasterRnn']:\n",
      "   943                                                   if not isinstance(last_window, pd.DataFrame):\n",
      "   944                                                       raise TypeError(\n",
      "   945                                                           f\"`last_window` must be a pandas DataFrame. Got {type(last_window)}.\"\n",
      "   946                                                       )\n",
      "   947                                           \n",
      "   948                                                   last_window_cols = last_window.columns.to_list()\n",
      "   949                                           \n",
      "   950                                                   if (\n",
      "   951                                                       forecaster_name in [\"ForecasterRecursiveMultiSeries\", \"ForecasterRnn\"]\n",
      "   952                                                       and len(set(levels) - set(last_window_cols)) != 0\n",
      "   953                                                   ):\n",
      "   954                                                       missing_levels = set(levels) - set(last_window_cols)\n",
      "   955                                                       raise ValueError(\n",
      "   956                                                           f\"`last_window` must contain a column(s) named as the level(s) to be predicted. \"\n",
      "   957                                                           f\"The following `levels` are missing in `last_window`: {missing_levels}\\n\"\n",
      "   958                                                           f\"Ensure that `last_window` contains all the necessary columns \"\n",
      "   959                                                           f\"corresponding to the `levels` being predicted.\\n\"\n",
      "   960                                                           f\"    Argument `levels`     : {levels}\\n\"\n",
      "   961                                                           f\"    `last_window` columns : {last_window_cols}\\n\"\n",
      "   962                                                           f\"Example: If `levels = ['series_1', 'series_2']`, make sure \"\n",
      "   963                                                           f\"`last_window` includes columns named 'series_1' and 'series_2'.\"\n",
      "   964                                                       )\n",
      "   965                                           \n",
      "   966                                                   if forecaster_name == 'ForecasterDirectMultiVariate':\n",
      "   967                                                       if len(set(series_names_in_) - set(last_window_cols)) > 0:\n",
      "   968                                                           raise ValueError(\n",
      "   969                                                               f\"`last_window` columns must be the same as the `series` \"\n",
      "   970                                                               f\"column names used to create the X_train matrix.\\n\"\n",
      "   971                                                               f\"    `last_window` columns    : {last_window_cols}\\n\"\n",
      "   972                                                               f\"    `series` columns X train : {series_names_in_}\"\n",
      "   973                                                           )\n",
      "   974                                               else:\n",
      "   975         1       8091.0   8091.0      0.4          if not isinstance(last_window, (pd.Series, pd.DataFrame)):\n",
      "   976                                                       raise TypeError(\n",
      "   977                                                           f\"`last_window` must be a pandas Series or DataFrame. \"\n",
      "   978                                                           f\"Got {type(last_window)}.\"\n",
      "   979                                                       )\n",
      "   980                                           \n",
      "   981                                               # Check last_window len, nulls and index (type and freq)\n",
      "   982         1      23071.0  23071.0      1.1      if len(last_window) < window_size:\n",
      "   983                                                   raise ValueError(\n",
      "   984                                                       f\"`last_window` must have as many values as needed to \"\n",
      "   985                                                       f\"generate the predictors. For this forecaster it is {window_size}.\"\n",
      "   986                                                   )\n",
      "   987         1     459089.0 459089.0     21.3      if last_window.isna().to_numpy().any():\n",
      "   988                                                   warnings.warn(\n",
      "   989                                                       \"`last_window` has missing values. Most of machine learning models do \"\n",
      "   990                                                       \"not allow missing values. Prediction method may fail.\", \n",
      "   991                                                       MissingValuesWarning\n",
      "   992                                                   )\n",
      "   993                                               \n",
      "   994         2      39577.0  19788.5      1.8      _, last_window_index = check_extract_values_and_index(\n",
      "   995         1       6189.0   6189.0      0.3          data=last_window, data_label='`last_window`', ignore_freq=False, return_values=False\n",
      "   996                                               )\n",
      "   997         1       6816.0   6816.0      0.3      if not isinstance(last_window_index, index_type_):\n",
      "   998                                                   raise TypeError(\n",
      "   999                                                       f\"Expected index of type {index_type_} for `last_window`. \"\n",
      "  1000                                                       f\"Got {type(last_window_index)}.\"\n",
      "  1001                                                   )\n",
      "  1002         1      26923.0  26923.0      1.2      if isinstance(last_window_index, pd.DatetimeIndex):\n",
      "  1003         1      76749.0  76749.0      3.6          if not last_window_index.freqstr == index_freq_:\n",
      "  1004                                                       raise TypeError(\n",
      "  1005                                                           f\"Expected frequency of type {index_freq_} for `last_window`. \"\n",
      "  1006                                                           f\"Got {last_window_index.freqstr}.\"\n",
      "  1007                                                       )\n",
      "  1008                                           \n",
      "  1009                                               # Checks exog\n",
      "  1010         1       7101.0   7101.0      0.3      if exog is not None:\n",
      "  1011                                           \n",
      "  1012                                                   # Check type, nulls and expected type\n",
      "  1013         1       7430.0   7430.0      0.3          if forecaster_name in ['ForecasterRecursiveMultiSeries']:\n",
      "  1014                                                       if not isinstance(exog, (pd.Series, pd.DataFrame, dict)):\n",
      "  1015                                                           raise TypeError(\n",
      "  1016                                                               f\"`exog` must be a pandas Series, DataFrame or dict. Got {type(exog)}.\"\n",
      "  1017                                                           )\n",
      "  1018                                                   else:\n",
      "  1019         1       8908.0   8908.0      0.4              if not isinstance(exog, (pd.Series, pd.DataFrame)):\n",
      "  1020                                                           raise TypeError(\n",
      "  1021                                                               f\"`exog` must be a pandas Series or DataFrame. Got {type(exog)}.\"\n",
      "  1022                                                           )\n",
      "  1023                                           \n",
      "  1024         1      10033.0  10033.0      0.5          if isinstance(exog, dict):\n",
      "  1025                                                       no_exog_levels = set(levels) - set(exog.keys())\n",
      "  1026                                                       if no_exog_levels:\n",
      "  1027                                                           warnings.warn(\n",
      "  1028                                                               f\"`exog` does not contain keys for levels {no_exog_levels}. \"\n",
      "  1029                                                               f\"Missing levels are filled with NaN. Most of machine learning \"\n",
      "  1030                                                               f\"models do not allow missing values. Prediction method may fail.\",\n",
      "  1031                                                               MissingExogWarning\n",
      "  1032                                                           )\n",
      "  1033                                                       exogs_to_check = [\n",
      "  1034                                                           (f\"`exog` for series '{k}'\", v) \n",
      "  1035                                                           for k, v in exog.items() \n",
      "  1036                                                           if v is not None and k in levels\n",
      "  1037                                                       ]\n",
      "  1038                                                   else:\n",
      "  1039         1       8643.0   8643.0      0.4              exogs_to_check = [('`exog`', exog)]\n",
      "  1040                                           \n",
      "  1041         1      24179.0  24179.0      1.1          last_step = max(steps) if isinstance(steps, list) else steps\n",
      "  1042         1     719565.0 719565.0     33.3          expected_index = expand_index(last_window_index, 1)[0]\n",
      "  1043         2      30221.0  15110.5      1.4          for exog_name, exog_to_check in exogs_to_check:\n",
      "  1044                                           \n",
      "  1045         1      10032.0  10032.0      0.5              if not isinstance(exog_to_check, (pd.Series, pd.DataFrame)):\n",
      "  1046                                                           raise TypeError(\n",
      "  1047                                                               f\"{exog_name} must be a pandas Series or DataFrame. Got {type(exog_to_check)}\"\n",
      "  1048                                                           )\n",
      "  1049                                           \n",
      "  1050         1     446712.0 446712.0     20.7              if exog_to_check.isna().to_numpy().any():\n",
      "  1051                                                           warnings.warn(\n",
      "  1052                                                               f\"{exog_name} has missing values. Most of machine learning models \"\n",
      "  1053                                                               f\"do not allow missing values. Prediction method may fail.\", \n",
      "  1054                                                               MissingValuesWarning\n",
      "  1055                                                           )\n",
      "  1056                                           \n",
      "  1057                                                       # Check exog has many values as distance to max step predicted\n",
      "  1058         1      15518.0  15518.0      0.7              if len(exog_to_check) < last_step:\n",
      "  1059                                                           if forecaster_name in ['ForecasterRecursiveMultiSeries']:\n",
      "  1060                                                               warnings.warn(\n",
      "  1061                                                                   f\"{exog_name} doesn't have as many values as steps \"\n",
      "  1062                                                                   f\"predicted, {last_step}. Missing values are filled \"\n",
      "  1063                                                                   f\"with NaN. Most of machine learning models do not \"\n",
      "  1064                                                                   f\"allow missing values. Prediction method may fail.\",\n",
      "  1065                                                                   MissingValuesWarning\n",
      "  1066                                                               )\n",
      "  1067                                                           else: \n",
      "  1068                                                               raise ValueError(\n",
      "  1069                                                                   f\"{exog_name} must have at least as many values as \"\n",
      "  1070                                                                   f\"steps predicted, {last_step}.\"\n",
      "  1071                                                               )\n",
      "  1072                                           \n",
      "  1073                                                       # Check name/columns are in exog_names_in_\n",
      "  1074         1      10361.0  10361.0      0.5              if isinstance(exog_to_check, pd.DataFrame):\n",
      "  1075         1      32508.0  32508.0      1.5                  col_missing = set(exog_names_in_).difference(set(exog_to_check.columns))\n",
      "  1076         1      12413.0  12413.0      0.6                  if col_missing:\n",
      "  1077                                                               if forecaster_name in ['ForecasterRecursiveMultiSeries']:\n",
      "  1078                                                                   warnings.warn(\n",
      "  1079                                                                       f\"{col_missing} not present in {exog_name}. All \"\n",
      "  1080                                                                       f\"values will be NaN.\",\n",
      "  1081                                                                       MissingExogWarning\n",
      "  1082                                                                   ) \n",
      "  1083                                                               else:\n",
      "  1084                                                                   raise ValueError(\n",
      "  1085                                                                       f\"Missing columns in {exog_name}. Expected {exog_names_in_}. \"\n",
      "  1086                                                                       f\"Got {exog_to_check.columns.to_list()}.\"\n",
      "  1087                                                                   )\n",
      "  1088                                                       else:\n",
      "  1089                                                           if exog_to_check.name is None:\n",
      "  1090                                                               raise ValueError(\n",
      "  1091                                                                   f\"When {exog_name} is a pandas Series, it must have a name. Got None.\"\n",
      "  1092                                                               )\n",
      "  1093                                           \n",
      "  1094                                                           if exog_to_check.name not in exog_names_in_:\n",
      "  1095                                                               if forecaster_name in ['ForecasterRecursiveMultiSeries']:\n",
      "  1096                                                                   warnings.warn(\n",
      "  1097                                                                       f\"'{exog_to_check.name}' was not observed during training. \"\n",
      "  1098                                                                       f\"{exog_name} is ignored. Exogenous variables must be one \"\n",
      "  1099                                                                       f\"of: {exog_names_in_}.\",\n",
      "  1100                                                                       IgnoredArgumentWarning\n",
      "  1101                                                                   )\n",
      "  1102                                                               else:\n",
      "  1103                                                                   raise ValueError(\n",
      "  1104                                                                       f\"'{exog_to_check.name}' was not observed during training. \"\n",
      "  1105                                                                       f\"Exogenous variables must be: {exog_names_in_}.\"\n",
      "  1106                                                                   )\n",
      "  1107                                           \n",
      "  1108                                                       # Check index dtype and freq\n",
      "  1109         2      29640.0  14820.0      1.4              _, exog_index = check_extract_values_and_index(\n",
      "  1110         1      11322.0  11322.0      0.5                  data=exog_to_check, data_label=exog_name, ignore_freq=True, return_values=False\n",
      "  1111                                                       )\n",
      "  1112         1      12091.0  12091.0      0.6              if not isinstance(exog_index, index_type_):\n",
      "  1113                                                           raise TypeError(\n",
      "  1114                                                               f\"Expected index of type {index_type_} for {exog_name}. \"\n",
      "  1115                                                               f\"Got {type(exog_index)}.\"\n",
      "  1116                                                           )\n",
      "  1117                                           \n",
      "  1118                                                       # Check exog starts one step ahead of last_window end.\n",
      "  1119         1      59515.0  59515.0      2.8              if expected_index != exog_index[0]:\n",
      "  1120                                                           if forecaster_name in ['ForecasterRecursiveMultiSeries']:\n",
      "  1121                                                               warnings.warn(\n",
      "  1122                                                                   f\"To make predictions {exog_name} must start one step \"\n",
      "  1123                                                                   f\"ahead of `last_window`. Missing values are filled \"\n",
      "  1124                                                                   f\"with NaN.\\n\"\n",
      "  1125                                                                   f\"    `last_window` ends at : {last_window.index[-1]}.\\n\"\n",
      "  1126                                                                   f\"    {exog_name} starts at : {exog_index[0]}.\\n\"\n",
      "  1127                                                                   f\"    Expected index : {expected_index}.\",\n",
      "  1128                                                                   MissingValuesWarning\n",
      "  1129                                                               )  \n",
      "  1130                                                           else:\n",
      "  1131                                                               raise ValueError(\n",
      "  1132                                                                   f\"To make predictions {exog_name} must start one step \"\n",
      "  1133                                                                   f\"ahead of `last_window`.\\n\"\n",
      "  1134                                                                   f\"    `last_window` ends at : {last_window.index[-1]}.\\n\"\n",
      "  1135                                                                   f\"    {exog_name} starts at : {exog_index[0]}.\\n\"\n",
      "  1136                                                                   f\"    Expected index : {expected_index}.\"\n",
      "  1137                                                               )\n",
      "  1138                                           \n",
      "  1139                                               # Checks ForecasterSarimax\n",
      "  1140         1      25189.0  25189.0      1.2      if forecaster_name == 'ForecasterSarimax':\n",
      "  1141                                                   # Check last_window_exog type, len, nulls and index (type and freq)\n",
      "  1142                                                   if last_window_exog is not None:\n",
      "  1143                                                       if not exog_in_:\n",
      "  1144                                                           raise ValueError(\n",
      "  1145                                                               \"Forecaster trained without exogenous variable/s. \"\n",
      "  1146                                                               \"`last_window_exog` must be `None` when predicting.\"\n",
      "  1147                                                           )\n",
      "  1148                                           \n",
      "  1149                                                       if not isinstance(last_window_exog, (pd.Series, pd.DataFrame)):\n",
      "  1150                                                           raise TypeError(\n",
      "  1151                                                               f\"`last_window_exog` must be a pandas Series or a \"\n",
      "  1152                                                               f\"pandas DataFrame. Got {type(last_window_exog)}.\"\n",
      "  1153                                                           )\n",
      "  1154                                                       if len(last_window_exog) < window_size:\n",
      "  1155                                                           raise ValueError(\n",
      "  1156                                                               f\"`last_window_exog` must have as many values as needed to \"\n",
      "  1157                                                               f\"generate the predictors. For this forecaster it is {window_size}.\"\n",
      "  1158                                                           )\n",
      "  1159                                                       if last_window_exog.isna().to_numpy().any():\n",
      "  1160                                                           warnings.warn(\n",
      "  1161                                                               \"`last_window_exog` has missing values. Most of machine learning \"\n",
      "  1162                                                               \"models do not allow missing values. Prediction method may fail.\",\n",
      "  1163                                                               MissingValuesWarning\n",
      "  1164                                                       )\n",
      "  1165                                                       _, last_window_exog_index = check_extract_values_and_index(\n",
      "  1166                                                           data=last_window_exog, data_label='`last_window_exog`', return_values=False\n",
      "  1167                                                       )\n",
      "  1168                                                       if not isinstance(last_window_exog_index, index_type_):\n",
      "  1169                                                           raise TypeError(\n",
      "  1170                                                               f\"Expected index of type {index_type_} for `last_window_exog`. \"\n",
      "  1171                                                               f\"Got {type(last_window_exog_index)}.\"\n",
      "  1172                                                           )\n",
      "  1173                                                       if isinstance(last_window_exog_index, pd.DatetimeIndex):\n",
      "  1174                                                           if not last_window_exog_index.freqstr == index_freq_:\n",
      "  1175                                                               raise TypeError(\n",
      "  1176                                                                   f\"Expected frequency of type {index_freq_} for \"\n",
      "  1177                                                                   f\"`last_window_exog`. Got {last_window_exog_index.freqstr}.\"\n",
      "  1178                                                               )\n",
      "  1179                                           \n",
      "  1180                                                       # Check all columns are in the pd.DataFrame, last_window_exog\n",
      "  1181                                                       if isinstance(last_window_exog, pd.DataFrame):\n",
      "  1182                                                           col_missing = set(exog_names_in_).difference(set(last_window_exog.columns))\n",
      "  1183                                                           if col_missing:\n",
      "  1184                                                               raise ValueError(\n",
      "  1185                                                                   f\"Missing columns in `last_window_exog`. Expected {exog_names_in_}. \"\n",
      "  1186                                                                   f\"Got {last_window_exog.columns.to_list()}.\"\n",
      "  1187                                                               )\n",
      "  1188                                                       else:\n",
      "  1189                                                           if last_window_exog.name is None:\n",
      "  1190                                                               raise ValueError(\n",
      "  1191                                                                   \"When `last_window_exog` is a pandas Series, it must have a \"\n",
      "  1192                                                                   \"name. Got None.\"\n",
      "  1193                                                               )\n",
      "  1194                                           \n",
      "  1195                                                           if last_window_exog.name not in exog_names_in_:\n",
      "  1196                                                               raise ValueError(\n",
      "  1197                                                                   f\"'{last_window_exog.name}' was not observed during training. \"\n",
      "  1198                                                                   f\"Exogenous variables must be: {exog_names_in_}.\"\n",
      "  1199                                                               )"
     ]
    }
   ],
   "source": [
    "# skforecast 0.17.0\n",
    "# ==============================================================================\n",
    "def funt_to_profile(forecaster, steps, exog):\n",
    "    check_predict_input(\n",
    "    forecaster_name  = type(forecaster).__name__,\n",
    "    steps            = steps,\n",
    "    is_fitted        = forecaster.is_fitted,\n",
    "    exog_in_         = forecaster.exog_in_,\n",
    "    index_type_      = forecaster.index_type_,\n",
    "    index_freq_      = forecaster.index_freq_,\n",
    "    window_size      = forecaster.window_size,\n",
    "    last_window      = forecaster.last_window_,\n",
    "    exog             = exog,\n",
    "    exog_names_in_   = forecaster.exog_names_in_,\n",
    "    interval         = None\n",
    ")\n",
    "\n",
    "\n",
    "%lprun -f check_predict_input funt_to_profile(forecaster, 100, exog_prediction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.12 ms ± 114 μs per loop (mean ± std. dev. of 150 runs, 100 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit -n 100 -r 150\n",
    "\n",
    "forecaster._create_predict_inputs(\n",
    "    steps=100, exog=exog_prediction\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Timer unit: 1e-09 s\n",
      "\n",
      "Total time: 0.00959741 s\n",
      "File: /home/joaquin/Documents/GitHub/skforecast/skforecast/recursive/_forecaster_recursive.py\n",
      "Function: _create_predict_inputs at line 1113\n",
      "\n",
      "Line #      Hits         Time  Per Hit   % Time  Line Contents\n",
      "==============================================================\n",
      "  1113                                               def _create_predict_inputs(\n",
      "  1114                                                   self,\n",
      "  1115                                                   steps: int | str | pd.Timestamp, \n",
      "  1116                                                   last_window: pd.Series | pd.DataFrame | None = None,\n",
      "  1117                                                   exog: pd.Series | pd.DataFrame | None = None,\n",
      "  1118                                                   predict_probabilistic: bool = False,\n",
      "  1119                                                   use_in_sample_residuals: bool = True,\n",
      "  1120                                                   use_binned_residuals: bool = True,\n",
      "  1121                                                   check_inputs: bool = True\n",
      "  1122                                               ) -> tuple[np.ndarray, np.ndarray | None, pd.Index, int]:\n",
      "  1123                                                   \"\"\"\n",
      "  1124                                                   Create the inputs needed for the first iteration of the prediction \n",
      "  1125                                                   process. As this is a recursive process, the last window is updated at \n",
      "  1126                                                   each iteration of the prediction process.\n",
      "  1127                                                   \n",
      "  1128                                                   Parameters\n",
      "  1129                                                   ----------\n",
      "  1130                                                   steps : int, str, pandas Timestamp\n",
      "  1131                                                       Number of steps to predict. \n",
      "  1132                                                       \n",
      "  1133                                                       - If steps is int, number of steps to predict. \n",
      "  1134                                                       - If str or pandas Datetime, the prediction will be up to that date.\n",
      "  1135                                                   last_window : pandas Series, pandas DataFrame, default None\n",
      "  1136                                                       Series values used to create the predictors (lags) needed in the \n",
      "  1137                                                       first iteration of the prediction (t + 1).\n",
      "  1138                                                       If `last_window = None`, the values stored in `self.last_window_` are\n",
      "  1139                                                       used to calculate the initial predictors, and the predictions start\n",
      "  1140                                                       right after training data.\n",
      "  1141                                                   exog : pandas Series, pandas DataFrame, default None\n",
      "  1142                                                       Exogenous variable/s included as predictor/s.\n",
      "  1143                                                   predict_probabilistic : bool, default False\n",
      "  1144                                                       If `True`, the necessary checks for probabilistic predictions will be \n",
      "  1145                                                       performed.\n",
      "  1146                                                   use_in_sample_residuals : bool, default True\n",
      "  1147                                                       If `True`, residuals from the training data are used as proxy of\n",
      "  1148                                                       prediction error to create predictions. \n",
      "  1149                                                       If `False`, out of sample residuals (calibration) are used. \n",
      "  1150                                                       Out-of-sample residuals must be precomputed using Forecaster's\n",
      "  1151                                                       `set_out_sample_residuals()` method.\n",
      "  1152                                                   use_binned_residuals : bool, default True\n",
      "  1153                                                       If `True`, residuals are selected based on the predicted values \n",
      "  1154                                                       (binned selection).\n",
      "  1155                                                       If `False`, residuals are selected randomly.\n",
      "  1156                                                   check_inputs : bool, default True\n",
      "  1157                                                       If `True`, the input is checked for possible warnings and errors \n",
      "  1158                                                       with the `check_predict_input` function. This argument is created \n",
      "  1159                                                       for internal use and is not recommended to be changed.\n",
      "  1160                                           \n",
      "  1161                                                   Returns\n",
      "  1162                                                   -------\n",
      "  1163                                                   last_window_values : numpy ndarray\n",
      "  1164                                                       Series values used to create the predictors needed in the first \n",
      "  1165                                                       iteration of the prediction (t + 1).\n",
      "  1166                                                   exog_values : numpy ndarray, None\n",
      "  1167                                                       Exogenous variable/s included as predictor/s.\n",
      "  1168                                                   prediction_index : pandas Index\n",
      "  1169                                                       Index of the predictions.\n",
      "  1170                                                   steps: int\n",
      "  1171                                                       Number of future steps predicted.\n",
      "  1172                                                   \n",
      "  1173                                                   \"\"\"\n",
      "  1174                                           \n",
      "  1175         1       3179.0   3179.0      0.0          if last_window is None:\n",
      "  1176         1       1912.0   1912.0      0.0              last_window = self.last_window_\n",
      "  1177                                           \n",
      "  1178         1       1030.0   1030.0      0.0          if self.is_fitted:\n",
      "  1179         2      46248.0  23124.0      0.5              steps = date_to_index_position(\n",
      "  1180         1       6942.0   6942.0      0.1                          index        = last_window.index,\n",
      "  1181         1        763.0    763.0      0.0                          date_input   = steps,\n",
      "  1182         1        798.0    798.0      0.0                          method       = 'prediction',\n",
      "  1183         1        490.0    490.0      0.0                          date_literal = 'steps'\n",
      "  1184                                                               )\n",
      "  1185                                           \n",
      "  1186         1        732.0    732.0      0.0          if check_inputs:\n",
      "  1187         2    2922621.0    1e+06     30.5              check_predict_input(\n",
      "  1188         1       4492.0   4492.0      0.0                  forecaster_name = type(self).__name__,\n",
      "  1189         1        599.0    599.0      0.0                  steps           = steps,\n",
      "  1190         1        791.0    791.0      0.0                  is_fitted       = self.is_fitted,\n",
      "  1191         1        428.0    428.0      0.0                  exog_in_        = self.exog_in_,\n",
      "  1192         1        700.0    700.0      0.0                  index_type_     = self.index_type_,\n",
      "  1193         1        820.0    820.0      0.0                  index_freq_     = self.index_freq_,\n",
      "  1194         1        595.0    595.0      0.0                  window_size     = self.window_size,\n",
      "  1195         1        666.0    666.0      0.0                  last_window     = last_window,\n",
      "  1196         1        570.0    570.0      0.0                  exog            = exog,\n",
      "  1197         1        751.0    751.0      0.0                  exog_names_in_  = self.exog_names_in_,\n",
      "  1198         1        492.0    492.0      0.0                  interval        = None\n",
      "  1199                                                       )\n",
      "  1200                                           \n",
      "  1201         1        717.0    717.0      0.0              if predict_probabilistic:\n",
      "  1202                                                           check_residuals_input(\n",
      "  1203                                                               forecaster_name              = type(self).__name__,\n",
      "  1204                                                               use_in_sample_residuals      = use_in_sample_residuals,\n",
      "  1205                                                               in_sample_residuals_         = self.in_sample_residuals_,\n",
      "  1206                                                               out_sample_residuals_        = self.out_sample_residuals_,\n",
      "  1207                                                               use_binned_residuals         = use_binned_residuals,\n",
      "  1208                                                               in_sample_residuals_by_bin_  = self.in_sample_residuals_by_bin_,\n",
      "  1209                                                               out_sample_residuals_by_bin_ = self.out_sample_residuals_by_bin_\n",
      "  1210                                                           )\n",
      "  1211                                           \n",
      "  1212         1        525.0    525.0      0.0          last_window_values = (\n",
      "  1213         1     525146.0 525146.0      5.5              last_window.iloc[-self.window_size:].to_numpy(copy=True).ravel()\n",
      "  1214                                                   )\n",
      "  1215         2      23596.0  11798.0      0.2          last_window_values = transform_numpy(\n",
      "  1216         1        388.0    388.0      0.0                                   array             = last_window_values,\n",
      "  1217         1        497.0    497.0      0.0                                   transformer       = self.transformer_y,\n",
      "  1218         1        345.0    345.0      0.0                                   fit               = False,\n",
      "  1219         1        291.0    291.0      0.0                                   inverse_transform = False\n",
      "  1220                                                                        )\n",
      "  1221         1        666.0    666.0      0.0          if self.differentiation is not None:\n",
      "  1222                                                       last_window_values = self.differentiator.fit_transform(last_window_values)\n",
      "  1223                                           \n",
      "  1224         1        521.0    521.0      0.0          if exog is not None:\n",
      "  1225         1      10448.0  10448.0      0.1              exog = input_to_frame(data=exog, input_name='exog')\n",
      "  1226                                                       # TODO: only do the selections if columns are not already selected\n",
      "  1227                                                       # if not exog.columns.equals(pd.Index(self.exog_names_in_)):\n",
      "  1228                                                       #     exog = exog[self.exog_names_in_]\n",
      "  1229         1    2133210.0    2e+06     22.2              exog = exog[self.exog_names_in_]\n",
      "  1230         2      21058.0  10529.0      0.2              exog = transform_dataframe(\n",
      "  1231         1        348.0    348.0      0.0                         df                = exog,\n",
      "  1232         1        624.0    624.0      0.0                         transformer       = self.transformer_exog,\n",
      "  1233         1        352.0    352.0      0.0                         fit               = False,\n",
      "  1234         1        295.0    295.0      0.0                         inverse_transform = False\n",
      "  1235                                                              )\n",
      "  1236                                                       # TODO: only check dtypes if they are not the same as seen in training\n",
      "  1237                                                       # if not exog.dtypes.to_dict() == self.exog_dtypes_out_:\n",
      "  1238                                                       #   check_exog_dtypes(exog=exog)\n",
      "  1239                                                       # else:\n",
      "  1240                                                       #     check_exog(exog=exog, allow_nan=False, series_id=series_id)\n",
      "  1241         1    3183946.0    3e+06     33.2              check_exog_dtypes(exog=exog)\n",
      "  1242         1     238932.0 238932.0      2.5              exog_values = exog.to_numpy()[:steps]\n",
      "  1243                                                   else:\n",
      "  1244                                                       exog_values = None\n",
      "  1245                                           \n",
      "  1246         2     452751.0 226375.5      4.7          prediction_index = expand_index(\n",
      "  1247         1       2241.0   2241.0      0.0                                 index = last_window.index,\n",
      "  1248         1        676.0    676.0      0.0                                 steps = steps,\n",
      "  1249                                                                      )\n",
      "  1250                                           \n",
      "  1251         1       4217.0   4217.0      0.0          return last_window_values, exog_values, prediction_index, steps"
     ]
    }
   ],
   "source": [
    "# skforecast 0.17.0\n",
    "# ==============================================================================\n",
    "def funt_to_profile(steps, exog):\n",
    "    forecaster._create_predict_inputs(\n",
    "        steps=steps, exog=exog\n",
    "    )\n",
    "\n",
    "\n",
    "%lprun -f forecaster._create_predict_inputs funt_to_profile(100, exog_prediction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "16 ms ± 445 μs per loop (mean ± std. dev. of 10 runs, 20 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit -n 20 -r 10\n",
    "\n",
    "# skforecast 0.17.0\n",
    "forecaster.predict(\n",
    "    steps=100, exog=exog_prediction\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Timer unit: 1e-09 s\n",
      "\n",
      "Total time: 0.0785089 s\n",
      "File: /home/joaquin/Documents/GitHub/skforecast/skforecast/recursive/_forecaster_recursive.py\n",
      "Function: predict at line 1457\n",
      "\n",
      "Line #      Hits         Time  Per Hit   % Time  Line Contents\n",
      "==============================================================\n",
      "  1457                                               def predict(\n",
      "  1458                                                   self,\n",
      "  1459                                                   steps: int | str | pd.Timestamp,\n",
      "  1460                                                   last_window: pd.Series | pd.DataFrame | None = None,\n",
      "  1461                                                   exog: pd.Series | pd.DataFrame | None = None,\n",
      "  1462                                                   check_inputs: bool = True\n",
      "  1463                                               ) -> pd.Series:\n",
      "  1464                                                   \"\"\"\n",
      "  1465                                                   Predict n steps ahead. It is an recursive process in which, each prediction,\n",
      "  1466                                                   is used as a predictor for the next step.\n",
      "  1467                                                   \n",
      "  1468                                                   Parameters\n",
      "  1469                                                   ----------\n",
      "  1470                                                   steps : int, str, pandas Timestamp\n",
      "  1471                                                       Number of steps to predict. \n",
      "  1472                                                       \n",
      "  1473                                                       - If steps is int, number of steps to predict. \n",
      "  1474                                                       - If str or pandas Datetime, the prediction will be up to that date.\n",
      "  1475                                                   last_window : pandas Series, pandas DataFrame, default None\n",
      "  1476                                                       Series values used to create the predictors (lags) needed in the \n",
      "  1477                                                       first iteration of the prediction (t + 1).\n",
      "  1478                                                       If `last_window = None`, the values stored in `self.last_window_` are\n",
      "  1479                                                       used to calculate the initial predictors, and the predictions start\n",
      "  1480                                                       right after training data.\n",
      "  1481                                                   exog : pandas Series, pandas DataFrame, default None\n",
      "  1482                                                       Exogenous variable/s included as predictor/s.\n",
      "  1483                                                   check_inputs : bool, default True\n",
      "  1484                                                       If `True`, the input is checked for possible warnings and errors \n",
      "  1485                                                       with the `check_predict_input` function. This argument is created \n",
      "  1486                                                       for internal use and is not recommended to be changed.\n",
      "  1487                                           \n",
      "  1488                                                   Returns\n",
      "  1489                                                   -------\n",
      "  1490                                                   predictions : pandas Series\n",
      "  1491                                                       Predicted values.\n",
      "  1492                                                   \n",
      "  1493                                                   \"\"\"\n",
      "  1494                                           \n",
      "  1495         1       1481.0   1481.0      0.0          (\n",
      "  1496         1        474.0    474.0      0.0              last_window_values,\n",
      "  1497         1        317.0    317.0      0.0              exog_values,\n",
      "  1498         1        307.0    307.0      0.0              prediction_index,\n",
      "  1499         1        334.0    334.0      0.0              steps\n",
      "  1500         2    7270302.0    4e+06      9.3          ) = self._create_predict_inputs(\n",
      "  1501         1        438.0    438.0      0.0                  steps        = steps,\n",
      "  1502         1        283.0    283.0      0.0                  last_window  = last_window,\n",
      "  1503         1        298.0    298.0      0.0                  exog         = exog,\n",
      "  1504         1        305.0    305.0      0.0                  check_inputs = check_inputs\n",
      "  1505                                                       )\n",
      "  1506                                           \n",
      "  1507         2      28401.0  14200.5      0.0          with warnings.catch_warnings():\n",
      "  1508         2      97352.0  48676.0      0.1              warnings.filterwarnings(\n",
      "  1509         1        421.0    421.0      0.0                  \"ignore\", \n",
      "  1510         1        543.0    543.0      0.0                  message=\"X does not have valid feature names\", \n",
      "  1511         1        764.0    764.0      0.0                  category=UserWarning\n",
      "  1512                                                       )\n",
      "  1513         2   70722780.0    4e+07     90.1              predictions = self._recursive_predict(\n",
      "  1514         1        376.0    376.0      0.0                                steps              = steps,\n",
      "  1515         1        356.0    356.0      0.0                                last_window_values = last_window_values,\n",
      "  1516         1        293.0    293.0      0.0                                exog_values        = exog_values\n",
      "  1517                                                                     )\n",
      "  1518                                           \n",
      "  1519         1       1541.0   1541.0      0.0          if self.differentiation is not None:\n",
      "  1520                                                       predictions = self.differentiator.inverse_transform_next_window(predictions)\n",
      "  1521                                           \n",
      "  1522         2       9854.0   4927.0      0.0          predictions = transform_numpy(\n",
      "  1523         1        497.0    497.0      0.0                            array             = predictions,\n",
      "  1524         1        479.0    479.0      0.0                            transformer       = self.transformer_y,\n",
      "  1525         1        551.0    551.0      0.0                            fit               = False,\n",
      "  1526         1        468.0    468.0      0.0                            inverse_transform = True\n",
      "  1527                                                                 )\n",
      "  1528                                           \n",
      "  1529         2     364548.0 182274.0      0.5          predictions = pd.Series(\n",
      "  1530         1        941.0    941.0      0.0                            data  = predictions,\n",
      "  1531         1       1063.0   1063.0      0.0                            index = prediction_index,\n",
      "  1532         1        704.0    704.0      0.0                            name  = 'pred'\n",
      "  1533                                                                 )\n",
      "  1534                                           \n",
      "  1535         1       2380.0   2380.0      0.0          return predictions"
     ]
    }
   ],
   "source": [
    "# skforecast 0.17.0\n",
    "# ==============================================================================\n",
    "def funt_to_profile(steps, exog):\n",
    "    forecaster.predict(\n",
    "        steps=steps, exog=exog\n",
    "    )\n",
    "\n",
    "\n",
    "%lprun -f forecaster.predict funt_to_profile(100, exog_prediction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\n",
    "    \"ignore\", \n",
    "    message=\".*X does not have valid feature names.*\", \n",
    "    category=UserWarning\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "last_window_values=forecaster.last_window_.to_numpy().flatten()\n",
    "exog_values=exog.to_numpy()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "13 ms ± 682 μs per loop (mean ± std. dev. of 20 runs, 20 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit -n 20 -r 20\n",
    "\n",
    "# skforecast 0.17.0\n",
    "forecaster._recursive_predict(\n",
    "    steps=100,\n",
    "    last_window_values=last_window_values,\n",
    "    exog_values=exog_values,\n",
    "    residuals=None,\n",
    "    use_binned_residuals=True\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Timer unit: 1e-09 s\n",
      "\n",
      "Total time: 0.0705477 s\n",
      "File: /home/joaquin/Documents/GitHub/skforecast/skforecast/recursive/_forecaster_recursive.py\n",
      "Function: _recursive_predict at line 1254\n",
      "\n",
      "Line #      Hits         Time  Per Hit   % Time  Line Contents\n",
      "==============================================================\n",
      "  1254                                               def _recursive_predict(\n",
      "  1255                                                   self,\n",
      "  1256                                                   steps: int,\n",
      "  1257                                                   last_window_values: np.ndarray,\n",
      "  1258                                                   exog_values: np.ndarray | None = None,\n",
      "  1259                                                   residuals: np.ndarray | dict[str, np.ndarray] | None = None,\n",
      "  1260                                                   use_binned_residuals: bool = True,\n",
      "  1261                                               ) -> np.ndarray:\n",
      "  1262                                                   \"\"\"\n",
      "  1263                                                   Predict n steps ahead. It is an iterative process in which, each prediction,\n",
      "  1264                                                   is used as a predictor for the next step.\n",
      "  1265                                                   \n",
      "  1266                                                   Parameters\n",
      "  1267                                                   ----------\n",
      "  1268                                                   steps : int\n",
      "  1269                                                       Number of steps to predict. \n",
      "  1270                                                   last_window_values : numpy ndarray\n",
      "  1271                                                       Series values used to create the predictors needed in the first \n",
      "  1272                                                       iteration of the prediction (t + 1).\n",
      "  1273                                                   exog_values : numpy ndarray, default None\n",
      "  1274                                                       Exogenous variable/s included as predictor/s.\n",
      "  1275                                                   residuals : numpy ndarray, dict, default None\n",
      "  1276                                                       Residuals used to generate bootstrapping predictions.\n",
      "  1277                                                   use_binned_residuals : bool, default True\n",
      "  1278                                                       If `True`, residuals are selected based on the predicted values \n",
      "  1279                                                       (binned selection).\n",
      "  1280                                                       If `False`, residuals are selected randomly.\n",
      "  1281                                           \n",
      "  1282                                                   Returns\n",
      "  1283                                                   -------\n",
      "  1284                                                   predictions : numpy ndarray\n",
      "  1285                                                       Predicted values.\n",
      "  1286                                                   \n",
      "  1287                                                   \"\"\"\n",
      "  1288                                           \n",
      "  1289         1      14989.0  14989.0      0.0          original_device = set_cpu_gpu_device(regressor=self.regressor, device='cpu')\n",
      "  1290                                           \n",
      "  1291         1       1664.0   1664.0      0.0          n_lags = len(self.lags) if self.lags is not None else 0\n",
      "  1292         1        330.0    330.0      0.0          n_window_features = (\n",
      "  1293                                                       len(self.X_train_window_features_names_out_)\n",
      "  1294         1        462.0    462.0      0.0              if self.window_features is not None\n",
      "  1295         1        365.0    365.0      0.0              else 0\n",
      "  1296                                                   )\n",
      "  1297         1       2624.0   2624.0      0.0          n_exog = exog_values.shape[1] if exog_values is not None else 0\n",
      "  1298                                           \n",
      "  1299         2      20932.0  10466.0      0.0          X = np.full(\n",
      "  1300         1       1033.0   1033.0      0.0              shape=(n_lags + n_window_features + n_exog), fill_value=np.nan, dtype=float\n",
      "  1301                                                   )\n",
      "  1302         1       7069.0   7069.0      0.0          predictions = np.full(shape=steps, fill_value=np.nan, dtype=float)\n",
      "  1303         1      10920.0  10920.0      0.0          last_window = np.concatenate((last_window_values, predictions))\n",
      "  1304                                           \n",
      "  1305       101      99998.0    990.1      0.1          for i in range(steps):\n",
      "  1306                                           \n",
      "  1307       100      88015.0    880.1      0.1              if self.lags is not None:\n",
      "  1308       100    1324038.0  13240.4      1.9                  X[:n_lags] = last_window[-self.lags - (steps - i)]\n",
      "  1309       100      70430.0    704.3      0.1              if self.window_features is not None:\n",
      "  1310                                                           X[n_lags : n_lags + n_window_features] = np.concatenate(\n",
      "  1311                                                               [\n",
      "  1312                                                                   wf.transform(last_window[i : -(steps - i)])\n",
      "  1313                                                                   for wf in self.window_features\n",
      "  1314                                                               ]\n",
      "  1315                                                           )\n",
      "  1316       100      54708.0    547.1      0.1              if exog_values is not None:\n",
      "  1317       100     308332.0   3083.3      0.4                  X[n_lags + n_window_features:] = exog_values[i]\n",
      "  1318                                                   \n",
      "  1319       100   67973804.0 679738.0     96.4              pred = self.regressor.predict(X.reshape(1, -1)).ravel()\n",
      "  1320                                                       \n",
      "  1321       100     103808.0   1038.1      0.1              if residuals is not None:\n",
      "  1322                                                           if use_binned_residuals:\n",
      "  1323                                                               predicted_bin = self.binner.transform(pred).item()\n",
      "  1324                                                               step_residual = residuals[predicted_bin][i]\n",
      "  1325                                                           else:\n",
      "  1326                                                               step_residual = residuals[i]\n",
      "  1327                                                           \n",
      "  1328                                                           pred += step_residual\n",
      "  1329                                                       \n",
      "  1330       100     298149.0   2981.5      0.4              predictions[i] = pred[0]\n",
      "  1331                                           \n",
      "  1332                                                       # Update `last_window` values. The first position is discarded and \n",
      "  1333                                                       # the new prediction is added at the end.\n",
      "  1334       100     152604.0   1526.0      0.2              last_window[-(steps - i)] = pred[0]\n",
      "  1335                                           \n",
      "  1336         1       8743.0   8743.0      0.0          set_cpu_gpu_device(regressor=self.regressor, device=original_device)\n",
      "  1337                                           \n",
      "  1338         1       4686.0   4686.0      0.0          return predictions"
     ]
    }
   ],
   "source": [
    "# skforecast 0.17.0\n",
    "# ==============================================================================\n",
    "\n",
    "import warnings\n",
    "warnings.filterwarnings(\n",
    "    \"ignore\", \n",
    "    message=\".*X does not have valid feature names.*\", \n",
    "    category=UserWarning\n",
    ")\n",
    "\n",
    "\n",
    "def funt_to_profile(steps, exog):\n",
    "    forecaster._recursive_predict(\n",
    "        steps=steps,\n",
    "        last_window_values=forecaster.last_window_.to_numpy().flatten(),\n",
    "        exog_values=exog.to_numpy(),\n",
    "        residuals = None,\n",
    "        use_binned_residuals=True\n",
    "    )\n",
    "\n",
    "%lprun -f forecaster._recursive_predict funt_to_profile(100, exog_prediction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  C_CONTIGUOUS : True\n",
      "  F_CONTIGUOUS : True\n",
      "  OWNDATA : True\n",
      "  WRITEABLE : True\n",
      "  ALIGNED : True\n",
      "  WRITEBACKIFCOPY : False\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "rng = np.random.default_rng(321)\n",
    "X_test = rng.random((1, len(forecaster.lags) + exog.shape[1])).astype(np.float64)\n",
    "print(X_test.flags)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "110 μs ± 3.87 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "forecaster.regressor.predict(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "225 ms ± 15.8 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit -n 10 -r 10\n",
    "\n",
    "# skforecast 0.16.0\n",
    "cv = TimeSeriesFold(\n",
    "    initial_train_size=int(len(y)/ 2),\n",
    "    steps=100,\n",
    "    fixed_train_size=False,\n",
    "    refit=False,\n",
    ")\n",
    "\n",
    "backtesting_forecaster(\n",
    "    forecaster=forecaster,\n",
    "    y=y,\n",
    "    exog=exog,\n",
    "    cv=cv,\n",
    "    metric='mean_squared_error',\n",
    "    show_progress=False,\n",
    "    verbose=False\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "skforecast_py12",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
